package org.qee.lock;

import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;

import java.util.Optional;
import java.util.concurrent.TimeUnit;

@Component
@Slf4j
public class RedisLockService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static int RETRY_COUNT = 3;

    private ResourceScriptSource unLockResource;

    public RedisLockService() {
        unLockResource = new ResourceScriptSource(new ClassPathResource("redis/redis_unlock.lua"));
    }

    /**
     * 加锁
     *
     * @param preKey
     * @param key
     * @param uniqueValue
     * @param expireTime
     * @return
     */
    public boolean tryLock(String preKey, String key, String uniqueValue, Long expireTime) {
        try {

            return Optional.ofNullable(redisTemplate.execute(new LockRedisCallback(getGlobalKey(preKey, key), uniqueValue, expireTime))).orElse(false);
        } catch (Throwable e) {
            log.error("RedisLockService-tryLock failed, prekey:{},key:{},uniqueValue:{},expireTime:{}", preKey, key, uniqueValue, expireTime);
        }
        return false;
    }

    /**
     * 普通解锁
     *
     * @param preKey
     * @param key
     * @param uniqueValue
     * @return
     */
    public boolean unLock(String preKey, String key, String uniqueValue) {
        String value = (String) redisTemplate.opsForValue().get(getGlobalKey(preKey, key));
        if (value == null || !value.equals(uniqueValue)) {
            log.warn("RedisLockService-unLock failed, unlock other lock, prekey:{},key:{},uniqueValue:{},oldUniqueValue:{}", preKey, key, value, uniqueValue);
            return false;
        }
        for (int i = 0; i < RETRY_COUNT; i++) {
            try {
                if (Optional.ofNullable(redisTemplate.delete(getGlobalKey(preKey, key))).orElse(false)) {
                    return true;
                }
            } catch (Throwable e) {
                log.error("RedisLockService-unLock exception, prekey:{},key:{},uniqueValue:{},oldUniqueValue:{}", preKey, key, value, uniqueValue, e);
            }
        }
        return false;
    }

    // lua 解锁

    public boolean unLockInLua(String preKey, String key, String uniqueValue) {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Long.class);
        redisScript.setScriptSource(unLockResource);
        Long execute = redisTemplate.execute(redisScript, Lists.newArrayList(getGlobalKey(preKey, key)), uniqueValue);
        return execute != null && execute == 1;
    }


    class LockRedisCallback implements RedisCallback<Boolean> {

        private String key;

        private String value;

        private Long expireTime;


        public LockRedisCallback(String key, String value, Long expireTime) {
            this.key = key;
            this.value = value;
            this.expireTime = expireTime;
        }

        @Override
        public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
            RedisSerializer<Object> valueSerializer = (RedisSerializer<Object>) redisTemplate.getValueSerializer();
            byte[] serializeValue = valueSerializer.serialize(value);
            Boolean result = redisConnection.set(key.getBytes(), serializeValue, Expiration.from(expireTime, TimeUnit.MILLISECONDS), RedisStringCommands.SetOption.SET_IF_ABSENT);
            return Optional.ofNullable(result).orElse(false);
        }
    }


    private String getGlobalKey(String preKey, String key) {
        return preKey + ":" + key;
    }

}
